| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732 |
2x
2x
2x
2x
2x
2x
2x
2x
2x
3305x
4215x
4215x
4215x
11737x
11737x
11737x
11737x
11737x
4215x
46x
4215x
30x
2x
80447x
2812x
2812x
2812x
23x
4x
19x
2789x
2789x
2789x
2789x
461x
461x
326x
2789x
2463x
2463x
80447x
2x
224x
224x
224x
224x
2x
120x
120x
120x
120x
2x
212x
2x
23x
2x
17x
2x
9039x
696x
696x
696x
68x
68x
696x
749x
749x
749x
696x
41x
41x
696x
19x
19x
696x
13x
13x
696x
9039x
2x
1994x
1994x
250x
1994x
110x
1994x
260x
1994x
166x
1994x
142x
1994x
2x
4746x
4746x
4746x
5213x
2x
4744x
4744x
545x
31x
4713x
6x
4707x
4707x
2x
48157x
52022x
52022x
52022x
8504x
4639x
4639x
2x
4764x
2x
4542x
2x
2952x
2x
3172x
266x
97x
3075x
2x
2399x
2x
4764x
4764x
401x
4363x
2x
4689x
3561x
2x
4687x
2x
4687x
189x
49x
4638x
2x
4638x
13x
4625x
9x
4616x
2x
76x
2x
2x
2x
2x
2x
2x
2x
2x
234x
7x
12x
156x
20x
39x
10x
2x
390x
2x
505x
2x
2x
282x
282x
282x
2x
175x
32x
32x
32x
32x
143x
143x
2x
138x
16x
122x
2x
154x
12x
28x
56x
32x
26x
2x
354x
2x
64x
2x
505x
505x
2x
326x
2x
2x
5x
2x
7x
7x
2x
2x
2x
8x
2x
20x
20x
2x
2x
5x
2x
7x
7x
2x
2x
2x
8x
2x
20x
20x
2x
2x
234x
5x
2x
3x
229x
5x
2x
3x
224x
2x
2x
2x
4x
2x
1127x
2x
2x
69x
2x
340x
360x
360x
340x
2x
83x
83x
83x
94x
94x
94x
40x
40x
54x
54x
54x
94x
14x
94x
55x
83x
2x
294x
294x
294x
294x
294x
294x
2x
2x
524x
524x
19x
524x
524x
2x
52022x
52022x
51772x
250x
2x
749x
2x
378x
2x
5230x
2x
2x
2x
| /**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Document } from '../model/document';
import { DocumentKey } from '../model/document_key';
import {
DoubleValue,
FieldValue,
NullValue,
RefValue
} from '../model/field_value';
import { FieldPath, ResourcePath } from '../model/path';
import { assert, fail } from '../util/assert';
import { Code, FirestoreError } from '../util/error';
import { isNullOrUndefined } from '../util/types';
export class Query {
static atPath(path: ResourcePath): Query {
return new Query(path);
}
private memoizedCanonicalId: string | null = null;
private memoizedOrderBy: OrderBy[] | null = null;
constructor(
readonly path: ResourcePath,
readonly explicitOrderBy: OrderBy[] = [],
readonly filters: Filter[] = [],
readonly limit: number | null = null,
readonly startAt: Bound | null = null,
readonly endAt: Bound | null = null
) {
if (this.startAt) {
this.assertValidBound(this.startAt);
}
if (this.endAt) {
this.assertValidBound(this.endAt);
}
}
get orderBy(): OrderBy[] {
if (this.memoizedOrderBy === null) {
const inequalityField = this.getInequalityFilterField();
const firstOrderByField = this.getFirstOrderByField();
if (inequalityField !== null && firstOrderByField === null) {
// In order to implicitly add key ordering, we must also add the
// inequality filter field for it to be a valid query.
// Note that the default inequality field and key ordering is ascending.
if (inequalityField.isKeyField()) {
this.memoizedOrderBy = [KEY_ORDERING_ASC];
} else {
this.memoizedOrderBy = [
new OrderBy(inequalityField),
KEY_ORDERING_ASC
];
}
} else {
assert(
inequalityField === null ||
(firstOrderByField !== null &&
inequalityField.isEqual(firstOrderByField)),
'First orderBy should match inequality field.'
);
this.memoizedOrderBy = [];
let foundKeyOrdering = false;
for (const orderBy of this.explicitOrderBy) {
this.memoizedOrderBy.push(orderBy);
if (orderBy.field.isKeyField()) {
foundKeyOrdering = true;
}
}
if (!foundKeyOrdering) {
// The order of the implicit key ordering always matches the last
// explicit order by
const lastDirection =
this.explicitOrderBy.length > 0
? this.explicitOrderBy[this.explicitOrderBy.length - 1].dir
: Direction.ASCENDING;
this.memoizedOrderBy.push(
lastDirection === Direction.ASCENDING
? KEY_ORDERING_ASC
: KEY_ORDERING_DESC
);
}
}
}
return this.memoizedOrderBy;
}
addFilter(filter: Filter): Query {
assert(
this.getInequalityFilterField() == null ||
!(filter instanceof RelationFilter) ||
!filter.isInequality() ||
filter.field.isEqual(this.getInequalityFilterField()!),
'Query must only have one inequality field.'
);
assert(
!DocumentKey.isDocumentKey(this.path),
'No filtering allowed for document query'
);
const newFilters = this.filters.concat([filter]);
return new Query(
this.path,
this.explicitOrderBy.slice(),
newFilters,
this.limit,
this.startAt,
this.endAt
);
}
addOrderBy(orderBy: OrderBy): Query {
assert(
!DocumentKey.isDocumentKey(this.path),
'No ordering allowed for document query'
);
assert(!this.startAt && !this.endAt, 'Bounds must be set after orderBy');
// TODO(dimond): validate that orderBy does not list the same key twice.
const newOrderBy = this.explicitOrderBy.concat([orderBy]);
return new Query(
this.path,
newOrderBy,
this.filters.slice(),
this.limit,
this.startAt,
this.endAt
);
}
withLimit(limit: number | null): Query {
return new Query(
this.path,
this.explicitOrderBy.slice(),
this.filters.slice(),
limit,
this.startAt,
this.endAt
);
}
withStartAt(bound: Bound): Query {
return new Query(
this.path,
this.explicitOrderBy.slice(),
this.filters.slice(),
this.limit,
bound,
this.endAt
);
}
withEndAt(bound: Bound): Query {
return new Query(
this.path,
this.explicitOrderBy.slice(),
this.filters.slice(),
this.limit,
this.startAt,
bound
);
}
// TODO(b/29183165): This is used to get a unique string from a query to, for
// example, use as a dictionary key, but the implementation is subject to
// collisions. Make it collision-free.
canonicalId(): string {
if (this.memoizedCanonicalId === null) {
let canonicalId = this.path.canonicalString();
canonicalId += '|f:';
for (const filter of this.filters) {
canonicalId += filter.canonicalId();
canonicalId += ',';
}
canonicalId += '|ob:';
// TODO(dimond): make this collision resistant
for (const orderBy of this.orderBy) {
canonicalId += orderBy.canonicalId();
canonicalId += ',';
}
if (!isNullOrUndefined(this.limit)) {
canonicalId += '|l:';
canonicalId += this.limit!;
}
if (this.startAt) {
canonicalId += '|lb:';
canonicalId += this.startAt.canonicalId();
}
if (this.endAt) {
canonicalId += '|ub:';
canonicalId += this.endAt.canonicalId();
}
this.memoizedCanonicalId = canonicalId;
}
return this.memoizedCanonicalId;
}
toString(): string {
let str = 'Query(' + this.path.canonicalString();
if (this.filters.length > 0) {
str += `, filters: [${this.filters.join(', ')}]`;
}
if (!isNullOrUndefined(this.limit)) {
str += ', limit: ' + this.limit;
}
if (this.explicitOrderBy.length > 0) {
str += `, orderBy: [${this.explicitOrderBy.join(', ')}]`;
}
if (this.startAt) {
str += ', startAt: ' + this.startAt.canonicalId();
}
if (this.endAt) {
str += ', endAt: ' + this.endAt.canonicalId();
}
return str + ')';
}
isEqual(other: Query): boolean {
Iif (this.limit !== other.limit) {
return false;
}
Iif (this.orderBy.length !== other.orderBy.length) {
return false;
}
for (let i = 0; i < this.orderBy.length; i++) {
if (!this.orderBy[i].isEqual(other.orderBy[i])) {
return false;
}
}
Iif (this.filters.length !== other.filters.length) {
return false;
}
for (let i = 0; i < this.filters.length; i++) {
if (!this.filters[i].isEqual(other.filters[i])) {
return false;
}
}
if (!this.path.isEqual(other.path)) {
return false;
}
Iif (
this.startAt !== null
? !this.startAt.isEqual(other.startAt)
: other.startAt !== null
) {
return false;
}
return this.endAt !== null
? this.endAt.isEqual(other.endAt)
: other.endAt === null;
}
docComparator(d1: Document, d2: Document): number {
let comparedOnKeyField = false;
for (const orderBy of this.orderBy) {
const comp = orderBy.compare(d1, d2);
if (comp !== 0) return comp;
comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
}
// Assert that we actually compared by key
assert(
comparedOnKeyField,
"orderBy used that doesn't compare on key field"
);
return 0;
}
matches(doc: Document): boolean {
return (
this.matchesAncestor(doc) &&
this.matchesOrderBy(doc) &&
this.matchesFilters(doc) &&
this.matchesBounds(doc)
);
}
hasLimit(): boolean {
return !isNullOrUndefined(this.limit);
}
getFirstOrderByField(): FieldPath | null {
return this.explicitOrderBy.length > 0
? this.explicitOrderBy[0].field
: null;
}
getInequalityFilterField(): FieldPath | null {
for (const filter of this.filters) {
if (filter instanceof RelationFilter && filter.isInequality()) {
return filter.field;
}
}
return null;
}
isDocumentQuery(): boolean {
return DocumentKey.isDocumentKey(this.path) && this.filters.length === 0;
}
private matchesAncestor(doc: Document): boolean {
const docPath = doc.key.path;
if (DocumentKey.isDocumentKey(this.path)) {
// exact match for document queries
return this.path.isEqual(docPath);
} else {
// shallow ancestor queries by default
return (
this.path.isPrefixOf(docPath) && this.path.length === docPath.length - 1
);
}
}
/**
* A document must have a value for every ordering clause in order to show up
* in the results.
*/
private matchesOrderBy(doc: Document): boolean {
for (const orderBy of this.explicitOrderBy) {
// order by key always matches
if (
!orderBy.field.isKeyField() &&
doc.field(orderBy.field) === undefined
) {
return false;
}
}
return true;
}
private matchesFilters(doc: Document): boolean {
for (const filter of this.filters) {
if (!filter.matches(doc)) {
return false;
}
}
return true;
}
/**
* Makes sure a document is within the bounds, if provided.
*/
private matchesBounds(doc: Document): boolean {
if (this.startAt && !this.startAt.sortsBeforeDocument(this.orderBy, doc)) {
return false;
}
if (this.endAt && this.endAt.sortsBeforeDocument(this.orderBy, doc)) {
return false;
}
return true;
}
private assertValidBound(bound: Bound): void {
assert(
bound.position.length <= this.orderBy.length,
'Bound is longer than orderBy'
);
}
}
export interface Filter {
matches(doc: Document): boolean;
canonicalId(): string;
isEqual(filter: Filter): boolean;
}
export class RelationOp {
static LESS_THAN = new RelationOp('<');
static LESS_THAN_OR_EQUAL = new RelationOp('<=');
static EQUAL = new RelationOp('==');
static GREATER_THAN = new RelationOp('>');
static GREATER_THAN_OR_EQUAL = new RelationOp('>=');
static fromString(op: string): RelationOp {
switch (op) {
case '<':
return RelationOp.LESS_THAN;
case '<=':
return RelationOp.LESS_THAN_OR_EQUAL;
case '==':
return RelationOp.EQUAL;
case '>=':
return RelationOp.GREATER_THAN_OR_EQUAL;
case '>':
return RelationOp.GREATER_THAN;
default:
return fail('Unknown relation: ' + op);
}
}
constructor(public name: string) {}
toString(): string {
return this.name;
}
isEqual(other: RelationOp): boolean {
return this.name === other.name;
}
}
export class RelationFilter implements Filter {
constructor(
public field: FieldPath,
public op: RelationOp,
public value: FieldValue
) {}
matches(doc: Document): boolean {
if (this.field.isKeyField()) {
assert(
this.value instanceof RefValue,
'Comparing on key, but filter value not a RefValue'
);
const refValue = this.value as RefValue;
const comparison = DocumentKey.comparator(doc.key, refValue.key);
return this.matchesComparison(comparison);
} else {
const val = doc.field(this.field);
return val !== undefined && this.matchesValue(val);
}
}
private matchesValue(value: FieldValue): boolean {
// Only compare types with matching backend order (such as double and int).
if (this.value.typeOrder !== value.typeOrder) {
return false;
}
return this.matchesComparison(value.compareTo(this.value));
}
private matchesComparison(comparison: number) {
switch (this.op) {
case RelationOp.LESS_THAN:
return comparison < 0;
case RelationOp.LESS_THAN_OR_EQUAL:
return comparison <= 0;
case RelationOp.EQUAL:
return comparison === 0;
case RelationOp.GREATER_THAN:
return comparison > 0;
case RelationOp.GREATER_THAN_OR_EQUAL:
return comparison >= 0;
default:
return fail('Unknown relation op' + this.op);
}
}
isInequality(): boolean {
return this.op !== RelationOp.EQUAL;
}
canonicalId(): string {
// TODO(b/29183165): Technically, this won't be unique if two values have
// the same description, such as the int 3 and the string "3". So we should
// add the types in here somehow, too.
return (
this.field.canonicalString() + this.op.toString() + this.value.toString()
);
}
isEqual(other: Filter): boolean {
Eif (other instanceof RelationFilter) {
return (
this.op.isEqual(other.op) &&
this.field.isEqual(other.field) &&
this.value.isEqual(other.value)
);
} else {
return false;
}
}
toString(): string {
return `${this.field.canonicalString()} ${this.op} ${this.value.value()}`;
}
}
/**
* Filter that matches 'null' values.
*/
export class NullFilter implements Filter {
constructor(public field: FieldPath) {}
matches(doc: Document): boolean {
const val = doc.field(this.field);
return val !== undefined && val.value() === null;
}
canonicalId(): string {
return this.field.canonicalString() + ' IS null';
}
toString(): string {
return `${this.field.canonicalString()} IS null`;
}
isEqual(other: Filter): boolean {
Eif (other instanceof NullFilter) {
return this.field.isEqual(other.field);
} else {
return false;
}
}
}
/**
* Filter that matches 'NaN' values.
*/
export class NanFilter implements Filter {
constructor(public field: FieldPath) {}
matches(doc: Document): boolean {
const val = doc.field(this.field).value();
return typeof val === 'number' && isNaN(val);
}
canonicalId(): string {
return this.field.canonicalString() + ' IS NaN';
}
toString(): string {
return `${this.field.canonicalString()} IS NaN`;
}
isEqual(other: Filter): boolean {
Eif (other instanceof NanFilter) {
return this.field.isEqual(other.field);
} else {
return false;
}
}
}
/**
* Creates a filter based on the provided arguments.
*/
export function fieldFilter(
field: FieldPath,
op: RelationOp,
value: FieldValue
) {
if (value.isEqual(NullValue.INSTANCE)) {
if (op !== RelationOp.EQUAL) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Invalid query. You can only perform equals ' + 'comparisons on null.'
);
}
return new NullFilter(field);
} else if (value.isEqual(DoubleValue.NAN)) {
if (op !== RelationOp.EQUAL) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Invalid query. You can only perform equals ' + 'comparisons on NaN.'
);
}
return new NanFilter(field);
} else {
return new RelationFilter(field, op, value);
}
}
/**
* The direction of sorting in an order by.
*/
export class Direction {
static ASCENDING = new Direction('asc');
static DESCENDING = new Direction('desc');
private constructor(public name: string) {}
toString(): string {
return this.name;
}
}
/**
* Represents a bound of a query.
*
* The bound is specified with the given components representing a position and
* whether it's just before or just after the position (relative to whatever the
* query order is).
*
* The position represents a logical index position for a query. It's a prefix
* of values for the (potentially implicit) order by clauses of a query.
*
* Bound provides a function to determine whether a document comes before or
* after a bound. This is influenced by whether the position is just before or
* just after the provided values.
*/
export class Bound {
constructor(readonly position: FieldValue[], readonly before: boolean) {}
canonicalId(): string {
// TODO(b/29183165): Make this collision robust.
let canonicalId = this.before ? 'b:' : 'a:';
for (const component of this.position) {
canonicalId += component.toString();
}
return canonicalId;
}
/**
* Returns true if a document sorts before a bound using the provided sort
* order.
*/
sortsBeforeDocument(orderBy: OrderBy[], doc: Document): boolean {
assert(
this.position.length <= orderBy.length,
"Bound has more components than query's orderBy"
);
let comparison = 0;
for (let i = 0; i < this.position.length; i++) {
const orderByComponent = orderBy[i];
const component = this.position[i];
if (orderByComponent.field.isKeyField()) {
assert(
component instanceof RefValue,
'Bound has a non-key value where the key path is being used.'
);
comparison = DocumentKey.comparator(
(component as RefValue).key,
doc.key
);
} else {
const docValue = doc.field(orderByComponent.field);
assert(
docValue !== undefined,
'Field should exist since document matched the orderBy already.'
);
comparison = component.compareTo(docValue);
}
if (orderByComponent.dir === Direction.DESCENDING) {
comparison = comparison * -1;
}
if (comparison !== 0) {
break;
}
}
return this.before ? comparison <= 0 : comparison < 0;
}
isEqual(other: Bound | null): boolean {
Iif (other === null) {
return false;
}
Iif (
this.before !== other.before ||
this.position.length !== other.position.length
) {
return false;
}
for (let i = 0; i < this.position.length; i++) {
const thisPosition = this.position[i];
const otherPosition = other.position[i];
return thisPosition.isEqual(otherPosition);
}
return true;
}
}
/**
* An ordering on a field, in some Direction. Direction defaults to ASCENDING.
*/
export class OrderBy {
readonly dir: Direction;
private readonly isKeyOrderBy: boolean;
constructor(readonly field: FieldPath, dir?: Direction) {
if (dir === undefined) {
dir = Direction.ASCENDING;
}
this.dir = dir;
this.isKeyOrderBy = field.isKeyField();
}
compare(d1: Document, d2: Document): number {
const comparison = this.isKeyOrderBy
? Document.compareByKey(d1, d2)
: Document.compareByField(this.field, d1, d2);
switch (this.dir) {
case Direction.ASCENDING:
return comparison;
case Direction.DESCENDING:
return -1 * comparison;
default:
return fail('Unknown direction: ' + this.dir);
}
}
canonicalId(): string {
// TODO(b/29183165): Make this collision robust.
return this.field.canonicalString() + this.dir.toString();
}
toString(): string {
return `${this.field.canonicalString()} (${this.dir})`;
}
isEqual(other: OrderBy): boolean {
return this.dir === other.dir && this.field.isEqual(other.field);
}
}
const KEY_ORDERING_ASC = new OrderBy(FieldPath.keyField(), Direction.ASCENDING);
const KEY_ORDERING_DESC = new OrderBy(
FieldPath.keyField(),
Direction.DESCENDING
);
|